home *** CD-ROM | disk | FTP | other *** search
/ SuperHack / SuperHack CD.bin / CODING / DEMOS / OTLIGHT.ZIP / LIGHT.PAS < prev    next >
Encoding:
Pascal/Delphi Source File  |  1996-10-18  |  17.4 KB  |  535 lines

  1. {
  2.   3d engine. Rotates a flatshaded cube around 256 degrees. Pretty slow sofar
  3.   but a fairly accurate model was used this time. So no more getting colors
  4.   from the z-value anymore... By Vulture/OT.
  5. }
  6.  
  7. Program Rotations3d;
  8.  
  9. Uses Crt;                                { Used units (don't need much :-) }
  10.  
  11. Const VGA = $0a000;                      { VGA segment }
  12.       MaxPolys = 6;
  13.       MaxColors = 32;
  14.       Light_Vector: Array[0..2] of Integer = (0,0,-1);
  15.  
  16. Type Virtual = Array [1..64000] of byte;
  17.      VirtPtr = ^Virtual;                 { Pointer to virtual screen }
  18.      PolyObject = Array[1..MaxPolys,1..4,1..3] of Integer;
  19.      Vector = Array[0..2] of Real;       { Blaaaarck... reals! Nuke 'm! }
  20.  
  21. Const Size = 70;
  22.       Box: PolyObject =              { Our object (a simple box) }
  23.         (((-Size, -Size, Size),(Size , -Size, Size),(Size , Size , Size),(-Size, Size , Size)),
  24.          ((-Size, Size , -Size),(Size , Size , -Size),(Size , -Size, -Size),(-Size, -Size, -Size)),
  25.          ((-Size, Size , Size),(-Size, Size , -Size),(-Size, -Size, -Size),(-Size, -Size, Size)),
  26.          ((Size , -Size, Size),(Size , -Size, -Size),(Size , Size , -Size),(Size , Size , Size)),
  27.          ((Size , Size , Size),(Size , Size , -Size),(-Size, Size , -Size),(-Size, Size , Size)),
  28.          ((-Size, -Size, Size),(-Size, -Size, -Size),(Size , -Size, -Size),(Size , -Size, Size)));
  29.  
  30. Var Virscr: VirtPtr;
  31.     Vaddr: Word;                     { Segment value of virtual screen}
  32.     Sine: Array[0..255] of Integer;  { Contains sine&cosine values }
  33.     Points: PolyObject;              { Holds new x,y,z }
  34.     AverageZ: Array[1..MaxPolys] of Integer;  { Average Z values of polygons }
  35.     Order: Array[1..MaxPolys] of Integer; { Order in which to draw the polys }
  36.     Xlist: Array[0..199,1..2] of Integer; { Start/End x values for polygon }
  37.     X,Y,Z: Integer;                  { Variables for formula }
  38.     Xt,Yt,Zt: Integer;               { Temporary variables for x,y,z }
  39.     XAngle,YAngle,ZAngle: Byte;      { Angles to rotate around }
  40.     Zoff: Integer;                   { Distance from viewer }
  41.     XSin,XCos: Integer;              { Sine/cosine of angle to rotate around }
  42.     YSin,YCos: Integer;
  43.     ZSin,ZCos: Integer;
  44.     Key: Byte;                       { To intercept a keypress }
  45.  
  46. { ------ }
  47.  
  48.     F_Normals: Array[1..MaxPolys,0..2] of Integer; { All face normals (xyz) }
  49.     R_Normals: Array[1..MaxPolys,0..2] of Integer; { Rotated face normals }
  50.     V1,V2,V3: Vector;                { 3 coordinates of a polygon }
  51.     Vector1,Vector2,Vector3: Vector; { And 3 other ones }
  52.     Normal: Vector;                  { Normalized vector }
  53.  
  54. { ------ }
  55.  
  56. (* =========================== MEMORY ROUTINES ============================ *)
  57.  
  58. Procedure SetUpVirtual;     { Set up memory needed for virtual screen }
  59. Begin
  60.   GetMem(VirScr,64000);
  61.   Vaddr := Seg(VirScr^);
  62. End;
  63.  
  64. Procedure ShutDown;         { Free memory used by virtual screen }
  65. Begin
  66.   FreeMem(VirScr,64000);
  67. End;
  68.  
  69. (* =========================== GRAPHICS ROUTINES ========================== *)
  70.  
  71. Procedure VideoMode(Mode: Byte); Assembler;
  72. Asm
  73.   xor     ah,ah
  74.   mov     al,Mode
  75.   int     10h
  76. End;
  77.  
  78. Procedure WaitRetrace; Assembler;
  79. Asm
  80.   mov     dx,3dah
  81. @l1:
  82.   in      al,dx
  83.   and     al,08h
  84.   jnz     @l1
  85. @l2:
  86.   in      al,dx
  87.   and     al,08h
  88.   jz      @l2
  89. End;
  90.  
  91. Procedure ClearScreen(Color:Byte;Where:Word); Assembler;
  92. Asm
  93.   mov     ax,Where
  94.   mov     es,ax           { ES points to VGA or vitual screen }
  95.   xor     di,di           { Start at begin of screen }
  96.   cld
  97.   mov     al,Color        { Set color in ax }
  98.   mov     ah,al
  99.   mov     cx,32000        { Do the entire screen }
  100.   rep     stosw           { Store the word in ax to es:[di] }
  101. End;
  102.  
  103. Procedure Set_Color(Color,R,G,B: Byte); Assembler;
  104. Asm
  105.   mov     dx,03c8h
  106.   mov     al,Color        { Load colorvalue (0..255) to alter }
  107.   out     dx,al
  108.   inc     dx
  109.   mov     al,R            { Red }
  110.   out     dx,al
  111.   mov     al,G            { Green }
  112.   out     dx,al
  113.   mov     al,B            { Blue }
  114.   out     dx,al
  115. End;
  116.  
  117. Procedure FlipPage(source,dest:Word); Assembler;
  118. Asm
  119.   push    ds              { Save ds on stack }
  120.   mov     ax,[Source]
  121.   mov     ds,ax           { ds => source segment }
  122.   xor     si,si           { ds:si => source }
  123.   mov     ax,[Dest]
  124.   mov     es,ax           { es => destination segment }
  125.   xor     di,di           { es:di => destination }
  126.   mov     cx,16000        { Screen size = 64 kbytes = 16000 dwords }
  127.   db      66h
  128.   rep     movsw           { Copy ds:si to es:di }
  129.   pop     ds              { Restore ds }
  130. End;
  131.  
  132. Procedure Hline(x1,x2,y:Word;Color:Byte;Where:Word); Assembler;
  133. Asm
  134.   mov     ax,Where
  135.   mov     es,ax
  136.   mov     ax,y
  137.   mov     di,ax
  138.   shl     ax,8
  139.   shl     di,6
  140.   add     di,ax
  141.   add     di,x1
  142.  
  143.   mov     al,Color
  144.   mov     ah,al
  145.   mov     cx,x2
  146.   sub     cx,x1
  147.   shr     cx,1
  148.   jnc     @Start
  149.   stosb                   { Set extra byte if carry set }
  150. @Start:
  151.   rep     stosw           { Set all bytes }
  152. End;
  153.  
  154. Procedure ScanEdge(X1,Y1,X2,Y2: Integer);        { By Denthor/Asphyxia }
  155. Var Loop,X,Xstep,Temp: Integer;
  156. Begin
  157.   If Y1 = Y2 then Exit;
  158.   If Y1 > Y2 then                         { y1 must be smaller than y2 }
  159.   Begin
  160.     Temp := Y1;
  161.     Y1 := Y2;
  162.     Y2 := Temp;
  163.     Temp := X1;
  164.     X1 := X2;
  165.     X2 := Temp;
  166.   End;
  167.   Xstep := ((X2-X1) shl 8) div (Y2-Y1);   { Calculate gradient }
  168.   X := X1 shl 8;                          { Starting x value }
  169.   For Loop := Y1 to Y2 Do
  170.   Begin
  171.     If (X shr 8) < Xlist[Loop,1] then Xlist[Loop,1] := X shr 8;
  172.     If (X shr 8) > Xlist[Loop,2] then Xlist[Loop,2] := X shr 8;
  173.     Inc(X,Xstep);
  174.   End;
  175. End;
  176.  
  177. Procedure Draw_Polygon(X1,Y1,X2,Y2,X3,Y3,X4,Y4: Integer; Color: Byte; Where:Word);
  178. Var MinY,MaxY,Loop: Integer;
  179. Begin
  180.   Asm                                     { Set minx/maxx to extremes }
  181.      mov    si,offset Xlist
  182.      mov    cx,200
  183. @FillLoop:
  184.      mov    ax,320                        { Minx = 320 }
  185.      mov    ds:[si],ax
  186.      inc    si
  187.      inc    si
  188.      xor    ax,ax                         { Maxx = 0 }
  189.      mov    ds:[si],ax
  190.      inc    si
  191.      inc    si
  192.      loop   @FillLoop
  193.   End;
  194.   miny:=y1;
  195.   maxy:=y1;
  196.   if y2<miny then miny:=y2;
  197.   if y3<miny then miny:=y3;
  198.   if y4<miny then miny:=y4;
  199.   if y2>maxy then maxy:=y2;
  200.   if y3>maxy then maxy:=y3;
  201.   if y4>maxy then maxy:=y4;
  202.   ScanEdge(X1,Y1,X2,Y2);
  203.   ScanEdge(X2,Y2,X3,Y3);
  204.   ScanEdge(X3,Y3,X4,Y4);
  205.   ScanEdge(X4,Y4,X1,Y1);
  206.   For Loop := MinY to MaxY Do
  207.      HLine(Xlist[Loop,1],Xlist[Loop,2],Loop,Color,Where);
  208. End;
  209.  
  210. (* ============================== 3D ROUTINES ============================= *)
  211.  
  212. (*      THANX TO INOPIA/OT FOR INFO 'N CODE ON CALCULATING FACENORMALS      *)
  213.  
  214. {
  215.   Procedure: Vector_Substraction
  216.   Function : Calculate a vector by substracting the components (x,y,z) of two
  217.              other vectors from eachother.
  218. }
  219.  
  220. Procedure Vector_Substraction(Var A: Vector; B,C: Vector);
  221. Begin
  222.   A[0] := C[0] - B[0];             { x }
  223.   A[1] := C[1] - B[1];             { y }
  224.   A[2] := C[2] - B[2];             { z }
  225. End;
  226.  
  227. {
  228.   Procedure: Cross_Product
  229.   Function : Calculate a vector by multiplying the components of two other
  230.              vectors. This returns a vector which is at 90 degrees to the
  231.              surface of the polygon.
  232. }
  233.  
  234. Procedure Cross_Product(Var A: Vector; B,C: Vector);
  235. Begin
  236.   A[0]:=B[1]*C[2]-B[2]*C[1];
  237.   A[1]:=B[2]*C[0]-B[0]*C[2];
  238.   A[2]:=B[0]*C[1]-B[1]*C[0];
  239. End;
  240.  
  241. {
  242.   Procedure: Normalize_Vector
  243.   Function : Set the length of a vector to 1.
  244. }
  245.  
  246. Procedure Normalize_Vector(Var A: Vector; B: Vector);
  247. Var Temp: Real;
  248. Begin
  249.   Temp := SQRT(B[0]*B[0]+B[1]*B[1]+B[2]*B[2]);
  250.   A[0] := B[0] / Temp;
  251.   A[1] := B[1] / Temp;
  252.   A[2] := B[2] / Temp;
  253. End;
  254.  
  255. {
  256.   Procedure: Face_Normals
  257.  
  258.   Precalculate a facenormal by:
  259.  
  260.      - taking 3 coÖrdinates of a polygon
  261.      - substract those from each other to calculate 2 other vectors
  262.      - take the crossproduct of those vectors
  263.      - set the length of the returned vector to 1
  264.      - multiply returned value by maximum number of colors available
  265.  
  266.   And do this for all polygons.
  267. }
  268.  
  269. Procedure Face_Normals;
  270. Var Loop1: Integer;
  271. Begin
  272.   For Loop1 := 1 to MaxPolys do                 { Calculate all facenormals }
  273.   Begin
  274.     V1[0] := Box[Loop1,1,1];                    { Get 3 3d-coordinates }
  275.     V1[1] := Box[Loop1,1,2];
  276.     V1[2] := Box[Loop1,1,3];
  277.     V2[0] := Box[Loop1,2,1];
  278.     V2[1] := Box[Loop1,2,2];
  279.     V2[2] := Box[Loop1,2,3];
  280.     V3[0] := Box[Loop1,3,1];
  281.     V3[1] := Box[Loop1,3,2];
  282.     V3[2] := Box[Loop1,3,3];
  283.     Vector_Substraction(Vector1,V2,V1);         { Determine 2 vectors }
  284.     Vector_Substraction(Vector2,V3,V1);
  285.     Cross_Product(Vector3,Vector1,Vector2);     { Take their cross-product }
  286.     Normalize_Vector(Normal,Vector3);           { Normalize returned vector }
  287.     F_Normals[Loop1,0] := Round(Normal[0]*MaxColors);  { Store components }
  288.     F_Normals[Loop1,1] := Round(Normal[1]*MaxColors);
  289.     F_Normals[Loop1,2] := Round(Normal[2]*MaxColors);
  290.   End;
  291. End;
  292.  
  293. Procedure UpdateRotation(DeltaX,DeltaY,DeltaZ: Integer);
  294. Begin
  295.   XAngle := (Xangle+DeltaX) Mod 256;  { Add addition factors }
  296.   YAngle := (Yangle+DeltaY) Mod 256;
  297.   ZAngle := (Zangle+DeltaZ) Mod 256;
  298.  
  299.   Xsin := Sine[Xangle];               { Grab sine from sinetable }
  300.   Xcos := Sine[(Xangle+64) Mod 256];  { Add 64 to get cosine (neat trick!) }
  301.   Ysin := Sine[Yangle];
  302.   Ycos := Sine[(Yangle+64) Mod 256];
  303.   Zsin := Sine[Zangle];
  304.   Zcos := Sine[(Zangle+64) Mod 256];
  305. End;
  306.  
  307. Procedure GetOrgXYZ(Obj: PolyObject; Poly,Place: Integer);
  308. Begin
  309.   X := Obj[Poly,Place,1];          { Grabs our original x,y,z values }
  310.   Y := Obj[Poly,Place,2];
  311.   Z := Obj[Poly,PLace,3];
  312. End;
  313.  
  314. Procedure RotatePoint; Assembler;  { Rotates a point around all axis }
  315. Asm
  316. { Rotate around x-axis }
  317. { YT = Y * COS(xang) - Z * SIN(xang) / 256 }
  318. { ZT = Y * SIN(xang) + Z * COS(xang) / 256 }
  319. { Y = YT }
  320. { Z = ZT }
  321.     pusha
  322.     mov     ax,[Y]
  323.     mov     bx,[XCos]
  324.     imul    bx               { ax = Y * Cos(xang) }
  325.     mov     bp,ax
  326.     mov     ax,[Z]
  327.     mov     bx,[XSin]
  328.     imul    bx               { ax = Z * Sin(xang) }
  329.     sub     bp,ax            { bp = Y * Cos(xang) - Z * Sin(xang) }
  330.     sar     bp,8             { bp = Y * Cos(xang) - Z * Sin(xang) / 256 }
  331.     mov     [Yt],bp
  332.  
  333.     mov     ax,[Y]
  334.     mov     bx,[XSin]
  335.     imul    bx               { ax = Y * Sin(xang) }
  336.     mov     bp,ax
  337.     mov     ax,[Z]
  338.     mov     bx,[XCos]
  339.     imul    bx               { ax = Z * Cos(xang) }
  340.     add     bp,ax            { bp = Y * SIN(xang) + Z * COS(xang) }
  341.     sar     bp,8             { bp = Y * SIN(xang) + Z * COS(xang) / 256 }
  342.     mov     [Zt],bp
  343.  
  344.     mov     ax,[Yt]          { Switch values }
  345.     mov     [Y],ax
  346.     mov     ax,[Zt]
  347.     mov     [Z],ax
  348.  
  349. { Rotate around y-axis }
  350. { XT = X * COS(yang) - Z * SIN(yang) / 256 }
  351. { ZT = X * SIN(yang) + Z * COS(yang) / 256 }
  352. { X = XT }
  353. { Z = ZT }
  354.  
  355.     mov     ax,[X]
  356.     mov     bx,[YCos]
  357.     imul    bx               { ax = X * Cos(yang) }
  358.     mov     bp,ax
  359.     mov     ax,[Z]
  360.     mov     bx,[YSin]
  361.     imul    bx               { ax = Z * Sin(yang) }
  362.     sub     bp,ax            { bp = X * Cos(yang) - Z * Sin(yang) }
  363.     sar     bp,8             { bp = X * Cos(yang) - Z * Sin(yang) / 256 }
  364.     mov     [Xt],bp
  365.  
  366.     mov     ax,[X]
  367.     mov     bx,[YSin]
  368.     imul    bx               { ax = X * Sin(yang) }
  369.     mov     bp,ax
  370.     mov     ax,[Z]
  371.     mov     bx,[YCos]
  372.     imul    bx               { ax = Z * Cos(yang) }
  373.     add     bp,ax            { bp = X * SIN(yang) + Z * COS(yang) }
  374.     sar     bp,8             { bp = X * SIN(yang) + Z * COS(yang) / 256 }
  375.     mov     [Zt],bp
  376.  
  377.     mov     ax,[Xt]          { Switch values }
  378.     mov     [X],ax
  379.     mov     ax,[Zt]
  380.     mov     [Z],ax
  381.  
  382. { Rotate around z-axis }
  383. { XT = X * COS(zang) - Y * SIN(zang) / 256 }
  384. { YT = X * SIN(zang) + Y * COS(zang) / 256 }
  385. { X = XT }
  386. { Y = YT }
  387.  
  388.     mov     ax,[X]
  389.     mov     bx,[ZCos]
  390.     imul    bx               { ax = X * Cos(zang) }
  391.     mov     bp,ax
  392.     mov     ax,[Y]
  393.     mov     bx,[ZSin]
  394.     imul    bx               { ax = Y * Sin(zang) }
  395.     sub     bp,ax            { bp = X * Cos(zang) - Y * Sin(zang) }
  396.     sar     bp,8             { bp = X * Cos(zang) - Y * Sin(zang) / 256 }
  397.     mov     [Xt],bp
  398.  
  399.     mov     ax,[X]
  400.     mov     bx,[ZSin]
  401.     imul    bx               { ax = X * Sin(zang) }
  402.     mov     bp,ax
  403.     mov     ax,[Y]
  404.     mov     bx,[ZCos]
  405.     imul    bx               { ax = Y * Cos(zang) }
  406.     add     bp,ax            { bp = X * SIN(zang) + Y * COS(zang) }
  407.     sar     bp,8             { bp = X * SIN(zang) + Y * COS(zang) / 256 }
  408.     mov     [Yt],bp
  409.  
  410.     mov     ax,[Xt]          { Switch values }
  411.     mov     [X],ax
  412.     mov     ax,[Yt]
  413.     mov     [Y],ax
  414.     popa
  415. End;
  416.  
  417. Procedure SortPolygons;                           { Sort polys on Z }
  418. Var Loop1, Position, Temp: Integer;
  419. Begin
  420.   For Loop1 := 1 to MaxPolys Do Order[Loop1] := Loop1;  { Reset order }
  421.   Position := 1;
  422.   While Position < MaxPolys Do                    { Sort all polygons }
  423.   Begin
  424.     If AverageZ[Position] < AverageZ[Position+1] then
  425.     Begin
  426.       Temp := AverageZ[Position];                 { Swap Z values }
  427.       AverageZ[Position] := AverageZ[Position+1];
  428.       AverageZ[Position+1] := Temp;
  429.       Temp := Order[Position];                    { Swap polygon draw order }
  430.       Order[Position] := Order[Position+1];
  431.       Order[Position+1] := Temp;
  432.       Position := 0;                              { Reset counter }
  433.     End;
  434.     Position := Position + 1;                     { Compare next 2 values }
  435.   End;
  436. End;
  437.  
  438. Procedure Rotate_Object(Obj: PolyObject; Where: Word; Mx,My: Integer);
  439. Var Loop1,Loop2,Temp,Nr,Color,
  440.     X1,Y1,X2,Y2,X3,Y3,X4,Y4: Integer; { 4 points of polygon }
  441. Begin
  442.  
  443.   For Loop1 := 1 to MaxPolys Do       { Rotate the polygons }
  444.   Begin
  445.     For Loop2 := 1 to 4 Do            { All 4 3d-points of polygon }
  446.     Begin
  447.       GetOrgXYZ(Obj,Loop1,Loop2);     { Get the original x,y,z values }
  448.       RotatePoint;                    { Rotate the point around x,y,z }
  449.       Points[Loop1,Loop2,1] := X;     { Save new x,y,z }
  450.       Points[Loop1,Loop2,2] := Y;
  451.       Points[Loop1,Loop2,3] := Z;
  452.     End;
  453.     AverageZ[Loop1] := Points[Loop1,1,3]+Points[Loop1,2,3]+Points[Loop1,3,3]+Points[Loop1,4,3];
  454.   End;
  455.  
  456.   For Loop1 := 1 to MaxPolys Do       { Now rotate face normals }
  457.   Begin
  458.     X := F_Normals[Loop1,0];
  459.     Y := F_Normals[Loop1,1];
  460.     Z := F_Normals[Loop1,2];
  461.     RotatePoint;
  462.     R_Normals[Loop1,0] := X;
  463.     R_Normals[Loop1,1] := Y;
  464.     R_Normals[Loop1,2] := Z;
  465.   End;
  466.  
  467.   SortPolygons;                       { Sort the polygons on z values }
  468.  
  469.   For Loop1 := 1 to MaxPolys Do       { Draw the polygons }
  470.   Begin
  471.     Nr := Order[Loop1];               { # of polygon to draw }
  472.     Temp := Points[Nr,1,3]-Zoff;
  473.     X1 := ((Points[Nr,1,1] shl 8) div Temp) + Mx;
  474.     Y1 := ((Points[Nr,1,2] shl 8) div Temp) + My;
  475.     Temp := Points[Nr,2,3]-Zoff;
  476.     X2 := ((Points[Nr,2,1] shl 8) div Temp) + Mx;
  477.     Y2 := ((Points[Nr,2,2] shl 8) div Temp) + My;
  478.     Temp := Points[Nr,3,3]-Zoff;
  479.     X3 := ((Points[Nr,3,1] shl 8) div Temp) + Mx;
  480.     Y3 := ((Points[Nr,3,2] shl 8) div Temp) + My;
  481.     Temp := Points[Nr,4,3]-Zoff;
  482.     X4 := ((Points[Nr,4,1] shl 8) div Temp) + Mx;
  483.     Y4 := ((Points[Nr,4,2] shl 8) div Temp) + My;
  484.     { Get color by taking the dotproduct of the facenormal & lightvector }
  485.     Color := Abs((R_Normals[Nr,0]*Light_Vector[0])+(R_Normals[Nr,1]*Light_Vector[1])+(R_Normals[Nr,2]*Light_Vector[2]));
  486.     Draw_Polygon(X1,Y1,X2,Y2,X3,Y3,X4,Y4,Color,Where);
  487.   End;
  488. End;
  489.  
  490. Procedure Setup;            { Program setup }
  491. Var Loop1: Byte;
  492. Begin
  493.   Randomize;
  494.   SetupVirtual;             { Setup memory for virtual screen }
  495.   For Loop1 := 0 to 255 Do  { Calculate sine values }
  496.      Sine[Loop1] := Round(Sin(Loop1*(2*pi/256))*256);
  497.   Xangle := Random(255);    { Set initial degrees }
  498.   Yangle := Random(255);
  499.   Zangle := Random(255);
  500.   Zoff := -500;             { Distance from viewer }
  501.   VideoMode($13);
  502.   For Loop1 := 0 to MaxColors Do   { Set palette for shading }
  503.      Set_Color(Loop1,Loop1,Loop1,Loop1);
  504.   Face_Normals;             { Pre-calculate face normals }
  505. End;
  506.  
  507. (* ============================= MAIN PROGRAM ============================= *)
  508.  
  509. Begin
  510.   Setup;                    { Program setup procedure }
  511.  
  512.   Repeat
  513.     UpdateRotation(2,1,1);  { Set new angles and fetch (co)sine data }
  514.     ClearScreen(0,Vaddr);   { Clear virtual screen }
  515.     Rotate_Object(Box,Vaddr,160,100); { Do all good stuff }
  516.     WaitRetrace;            { Wait for a vertical retrace }
  517.     FlipPage(Vaddr,VGA);    { And let's show the stuff on screen }
  518.     If Keypressed then Key := Ord(Readkey);   { Catch a keypress }
  519.   Until Key = 27;           { Quit on escape }
  520.  
  521.   ShutDown;                 { Free memory }
  522.   VideoMode($3);            { Warp back to textmode }
  523.   Writeln('▄  ▄▄  ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄  ▄▄  ▄');
  524.   Writeln('                    - An Outlaw Triad Production (c) 1996 -');
  525.   Writeln;
  526.   Writeln('                             Code∙∙∙∙∙∙∙∙∙∙Vulture');
  527.   Writeln('                             Text∙∙∙∙∙∙∙∙∙∙Inopia');
  528.   Writeln;
  529.   Writeln('                            -=≡ Outlaw Triad is ≡=-');
  530.   Writeln;
  531.   Writeln('  Vulture/code ■ Archangel/artist ■ Troop/sysop ■ Xplorer/artist ■ Inopia/code');
  532.   Writeln;
  533.   Writeln('▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄');
  534. End.
  535.